<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <title>Canvas 提示框自动避碰演示</title>
    <style>
      body {
        background: #f8f8f8;
      }
      canvas {
        background: #fff;
        border: 1px solid #ccc;
        display: block;
        margin: 40px auto;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="900" height="600"></canvas>
    <script>
      // 配置
      const POINT_COUNT = 20;
      const CANVAS_WIDTH = 900;
      const CANVAS_HEIGHT = 600;
      const LABEL_WIDTH = 90;
      const LABEL_HEIGHT = 36;
      const POINT_RADIUS = 6;

      // 四种提示框相对点的位置
      const POSITIONS = [
        {
          name: "top",
          dx: -LABEL_WIDTH / 2,
          dy: -LABEL_HEIGHT - POINT_RADIUS - 4,
        },
        { name: "bottom", dx: -LABEL_WIDTH / 2, dy: POINT_RADIUS + 4 },
        {
          name: "left",
          dx: -LABEL_WIDTH - POINT_RADIUS - 4,
          dy: -LABEL_HEIGHT / 2,
        },
        { name: "right", dx: POINT_RADIUS + 4, dy: -LABEL_HEIGHT / 2 },
      ];

      // 随机生成点
      function randomPoints(count) {
        const margin = 100;
        const points = [];
        for (let i = 0; i < count; i++) {
          points.push({
            x: Math.random() * (CANVAS_WIDTH - 2 * margin) + margin,
            y: Math.random() * (CANVAS_HEIGHT - 2 * margin) + margin,
            label: `点${i + 1}`,
          });
        }
        return points;
      }

      // 计算某点某位置的提示框矩形
      function getLabelRect(point, posIdx) {
        const pos = POSITIONS[posIdx];
        return {
          x: point.x + pos.dx,
          y: point.y + pos.dy,
          w: LABEL_WIDTH,
          h: LABEL_HEIGHT,
        };
      }

      // 判断两个矩形是否重叠
      function isRectOverlap(a, b) {
        return !(
          a.x + a.w <= b.x ||
          b.x + b.w <= a.x ||
          a.y + a.h <= b.y ||
          b.y + b.h <= a.y
        );
      }

      // 计算两个重叠矩形之间的斥力
      function calculateRepulsion(a, b) {
        const dx = b.x - a.x;
        const dy = b.y - a.y;
        const distance = Math.sqrt(dx * dx + dy * dy) + 0.001;
        return 1 / distance;
      }

      // 计算两个矩形之间的重叠面积
      function calculateOverlapArea(a, b) {
        const dx = Math.max(
          0,
          Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)
        );
        const dy = Math.max(
          0,
          Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y)
        );
        return dx * dy;
      }

      // 计算重叠权重
      function getOverlapWeight(a, b) {
        let weight = 0;

        if (isRectOverlap(a, b)) {
          // 若重叠，则计算斥力，将权重设置为斥力
          // weight = calculateRepulsion(a, b);
          weight = calculateOverlapArea(a, b);
        }

        return weight;
      }

      // 主算法：为每个点选择一个最不冲突的位置
      function assignLabelPositions(points) {
        const labelRects = [];
        const chosenPositions = [];
        for (let i = 0; i < points.length; i++) {
          let minOverlap = Infinity;
          let bestPos = 0;
          for (let posIdx = 0; posIdx < POSITIONS.length; posIdx++) {
            const rect = getLabelRect(points[i], posIdx);
            // 统计与已放置的提示框重叠的数量
            // let overlapCount = 0;
            // for (let j = 0; j < labelRects.length; j++) {
            //   if (isRectOverlap(rect, labelRects[j])) overlapCount++;
            // }
            // if (overlapCount < minOverlap) {
            //   minOverlap = overlapCount;
            //   bestPos = posIdx;
            // }
            // // 如果完全不重叠，直接用这个位置
            // if (overlapCount === 0) break;

            // 计算重叠权重
            let overlapWeight = 0;
            for (let j = 0; j < labelRects.length; j++) {
              overlapWeight += getOverlapWeight(rect, labelRects[j]);
            }
            if (overlapWeight < minOverlap) {
              minOverlap = overlapWeight;
              bestPos = posIdx;
            }

            // 如果重叠权重为0，则直接用这个位置
            if (overlapWeight === 0) break;
          }
          labelRects.push(getLabelRect(points[i], bestPos));
          chosenPositions.push(bestPos);
        }
        return chosenPositions;
      }

      // 绘制
      function draw(points, chosenPositions) {
        const ctx = document.getElementById("myCanvas").getContext("2d");
        ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        for (let i = 0; i < points.length; i++) {
          const pt = points[i];
          // 画点
          ctx.beginPath();
          ctx.arc(pt.x, pt.y, POINT_RADIUS, 0, 2 * Math.PI);
          ctx.fillStyle = "#1976d2";
          ctx.fill();
          ctx.strokeStyle = "#fff";
          ctx.lineWidth = 2;
          ctx.stroke();
          // 画提示框
          const posIdx = chosenPositions[i];
          const rect = getLabelRect(pt, posIdx);
          ctx.beginPath();
          ctx.rect(rect.x, rect.y, rect.w, rect.h);
          ctx.fillStyle = "rgba(255,255,255,0.95)";
          ctx.fill();
          ctx.strokeStyle = "#1976d2";
          ctx.lineWidth = 1.5;
          ctx.stroke();
          // 画文字
          ctx.fillStyle = "#333";
          ctx.font = "16px sans-serif";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillText(pt.label, rect.x + rect.w / 2, rect.y + rect.h / 2);
          // 画连线
          ctx.beginPath();
          let anchor = { x: pt.x, y: pt.y };
          if (posIdx === 0) anchor.y -= POINT_RADIUS; // 上
          if (posIdx === 1) anchor.y += POINT_RADIUS; // 下
          if (posIdx === 2) anchor.x -= POINT_RADIUS; // 左
          if (posIdx === 3) anchor.x += POINT_RADIUS; // 右
          ctx.moveTo(anchor.x, anchor.y);
          // 算提示框边的锚点
          let labelAnchor = { x: 0, y: 0 };
          if (posIdx === 0) {
            // 上
            labelAnchor.x = pt.x;
            labelAnchor.y = rect.y + rect.h;
          } else if (posIdx === 1) {
            // 下
            labelAnchor.x = pt.x;
            labelAnchor.y = rect.y;
          } else if (posIdx === 2) {
            // 左
            labelAnchor.x = rect.x + rect.w;
            labelAnchor.y = pt.y;
          } else {
            // 右
            labelAnchor.x = rect.x;
            labelAnchor.y = pt.y;
          }
          ctx.lineTo(labelAnchor.x, labelAnchor.y);
          ctx.strokeStyle = "#1976d2";
          ctx.lineWidth = 1;
          ctx.stroke();
        }
      }

      // 主流程
      const points = randomPoints(POINT_COUNT);
      const chosenPositions = assignLabelPositions(points);
      draw(points, chosenPositions);

      // 支持点击刷新
      window.addEventListener("click", () => {
        const pts = randomPoints(POINT_COUNT);
        const pos = assignLabelPositions(pts);
        draw(pts, pos);
      });
    </script>
  </body>
</html>
